package com.googlecode.mavenjavacodegenplugin;

/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.body.ModifierSet;
import japa.parser.ast.body.TypeDeclaration;
import japa.parser.ast.visitor.DumpVisitor;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;

/**
 * Goal which generates source files from a java model.
 * 
 * @author Sebastian Prehn <sebastian.prehn@planetswebdesign.de>
 *
 * @goal javacodegen
 * @phase generate-sources
 */
public class JavaCodegenMojo extends AbstractMojo {

    /**
     * The current Maven project.
     * 
     * @parameter default-value="${project}"
     * @readonly
     * @required
     */
    private MavenProject project;
    
    /**
     * The directory where the generated files will be stored. The directory will be registered as a
     * compile source root of the project such that the generated files will participate in later build phases like
     * compiling and packaging.
     * 
     * @parameter expression="${outputDirectory}" default-value="${project.build.directory}/generated-sources/javacodegen"
     */
    private File outputDirectory;
    
    /**
     * The model source from which to generate Java code.
     * This must be a fully quallified name of a calls in classpath that has a method with the following signature.
     * 
     * public static Set<CompilationUnit> getModel()
     * 
     * @parameter expression="${model}" 
     * @required
     */
    private String modelSource;

    @Override
    public void execute() throws MojoExecutionException {
        if (!this.outputDirectory.exists()) {
            this.outputDirectory.mkdirs();
        }

        for (CompilationUnit unit : getModel()) {
            final File folder;
            String folderPath = null;
            if (unit.getPackage() != null) {
                folderPath = unit.getPackage().getName().toString().replace('.', '/');
                folder = new File(this.outputDirectory, folderPath);
                if (!folder.exists()) {
                    getLog().debug(String.format("Creating output directory: %s", folderPath));
                    folder.mkdirs();
                }
            } else {
                folder = this.outputDirectory;
            }

            final String fileName = getFileName(unit);

            if (doesFileExist(folderPath, fileName)) {
                getLog().info(String.format("File already exists. Skipping generation of: %s", fileName));
                continue;
            }

            DumpVisitor dumpVisitor = new DumpVisitor();
            dumpVisitor.visit(unit, null);
            writeFile(new File(folder, fileName), dumpVisitor.getSource());
        }

        addSourceRoot(this.outputDirectory);
    }

    /**
     * Gets the model from according to modelSource configuration.
     * @return the model
     * @throws MojoExecutionException in case of any error such as misconfiguration or model generation
     */
    @SuppressWarnings({"unchecked"})
    private Set<CompilationUnit> getModel() throws MojoExecutionException {
        try {
            Class<?> clazz = Class.forName(modelSource);
            Method declaredMethod = clazz.getDeclaredMethod("getModel");
            return (Set<CompilationUnit>) declaredMethod.invoke(clazz);
        } catch (ClassNotFoundException ex) {
            throw new MojoExecutionException(String.format("Class %s not found. Please make sure to include a dependency configuration for this class in the codegen plugin configuration.", modelSource));
        } catch (NoSuchMethodException ex) {
            getLog().error(ex);
            throw new MojoExecutionException("Missing method with signature public static Set<CompilationUnit> getModel() on modelSource");
        } catch (IllegalAccessException ex) {
            getLog().error(ex);
            throw new MojoExecutionException("Missing method with signature public static Set<CompilationUnit> getModel() on modelSource");
        } catch (IllegalArgumentException ex) {
            getLog().error(ex);
            throw new MojoExecutionException("Missing method with signature public static Set<CompilationUnit> getModel() on modelSource");
        } catch (ClassCastException ex) {
            getLog().error(ex);
            throw new MojoExecutionException("Missing method with signature public static Set<CompilationUnit> getModel() on modelSource");
        } catch (InvocationTargetException ex) {
            getLog().error(ex.getTargetException());
            throw new MojoExecutionException("Exception encountered while getting java model.", ex);
        } 
    }

    /**
     * Gets the file name for the compilation unit.
     * @param unit the unit, not <code>null</code>
     * @return the file name, never <code>null</code>
     * @throws MojoExecutionException in case no filename can be identified
     */
    private String getFileName(CompilationUnit unit) throws MojoExecutionException {
        for (TypeDeclaration typeDeclaration : unit.getTypes()) {
            if ((typeDeclaration.getModifiers() & ModifierSet.PUBLIC) > 0) {
                return typeDeclaration.getName() + ".java";
            }
        }
        throw new MojoExecutionException("No public type found in compilation unit. This should not happen. Pleaes check your model.");
    }

    /**
     * Writes content to file.
     * @param file the file, not <code>null</code>
     * @param content, not <code>null</code>
     * @throws MojoExecutionException in case of an error
     */
    private void writeFile(File file, String content) throws MojoExecutionException {
        FileWriter w = null;
        try {
            w = new FileWriter(file);
            w.write(content);
        } catch (IOException e) {
            throw new MojoExecutionException("Error creating file " + file, e);
        } finally {
            if (w != null) {
                try {
                    w.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    /**
     * Checks in all source folders for a java file of same folderPath and fileName.
     * @param folderPath the path of the folder, this relates to the package name. May be <code>null</code>.
     * @param fileName the file name. Must not be <code>null</code>
     * @return true, iff the file already exists in one of the source folders
     */
    @SuppressWarnings("unchecked")
    private boolean doesFileExist(String folderPath, String fileName) {
        for (String compileSourceRoot : (List<String>) this.project.getCompileSourceRoots()) {
            File parentFolder;
            if (folderPath != null) {
                parentFolder = new File(compileSourceRoot, folderPath);
            } else {
                parentFolder = new File(compileSourceRoot);
            }
            if (new File(parentFolder, fileName).exists()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Registers the specified directory as a compile source root for the current project.
     * 
     * @param directory The absolute path to the source root, must not be <code>null</code>.
     */
    private void addSourceRoot(File directory) {
        if (this.project != null) {
            getLog().debug("Adding compile source root: " + directory);
            this.project.addCompileSourceRoot(directory.getAbsolutePath());
        }
    }
}
